Repository navigation
Keep non-ASCII unescaped in wire serialization - #1964
KarataevDmitry wants to merge 3 commits into
Conversation
|
You missed one writer: That writer serves resumability replay ( The new test only covers |
ae0c14f to
7a6472e
Compare
|
@dfedoryshchev Good catch — you're right on both counts. Fixed in 7a6472e: The new test now covers both writers: Also rebased onto current main (#1934 restructured |
MCP payloads are read by LLMs, not embedded into HTML. The conservative default encoder escapes every non-ASCII char into a six-character sequence, costing x2-3 tokens on Cyrillic/CJK tool results. Escaping is semantically neutral in JSON; MEAI (AIJsonUtilities) and ASP.NET Core already default to JavaScriptEncoder.UnsafeRelaxedJsonEscaping. - Set the relaxed encoder on McpJsonUtilities.DefaultOptions - Serialize wire messages (stdio server/client, HTTP client, SSE writer) via DefaultOptions instead of the frozen generated JsonContext options - Propagate the encoder into the SSE Utf8JsonWriter Addresses modelcontextprotocol#626. Signed-off-by: Каратаев Дмитрий <dkarataev1990@gmail.com>
The replay path (SseEventStreamReaderExtensions.CopyToAsync) created its Utf8JsonWriter without options, and a writer's encoder overrides JsonSerializerOptions.Encoder - so a reconnecting client with an event store received replayed events escaped while live ones were unescaped. Pass the same JsonWriterOptions as McpSseEventWriterExtensions, and cover both writers with an integration test that reads progress notifications from the live POST response stream and from a Last-Event-ID replay. Signed-off-by: Каратаев Дмитрий <dkarataev1990@gmail.com>
7a6472e to
028096a
Compare
Summary
Wire serialization (stdio server/client, HTTP client, SSE writer) goes through the frozen
source-generated
JsonContext.Default.Options, whose conservative encoder escapes everynon-ASCII character into a six-character sequence - a Cyrillic/CJK tool payload costs x2-3
tokens when the consumer is an LLM, not an HTML embedder.
Escaping is semantically neutral in JSON: clients parse
"б"and"б"into the identicalstring. MEAI (
AIJsonUtilities.DefaultOptions) and ASP.NET Core already default toJavaScriptEncoder.UnsafeRelaxedJsonEscaping; with this change MCP matches them.Changes
McpJsonUtilities.CreateDefaultOptions: set the relaxed encoder (next to the existingconverter/resolver customization, before
MakeReadOnly).McpJsonUtilities.DefaultOptions.GetTypeInfo<JsonRpcMessage>()instead of
JsonContext.Default.JsonRpcMessage:StreamServerTransport,StreamClientSessionTransport,McpHttpClient(both NET and non-NET paths),McpSseEventWriterExtensions.DefaultOptionsalready exists precisely to customize the context defaults, and[JsonSourceGenerationOptions]cannot express an encoder, so the hand-written options are theright single source for the wire.
Utf8JsonWriter(JsonWriterOptions.Encoder).Measurement
Real-world check (our AgentNotes KB MCP server, Russian/Chinese corpus,
read_hot_contexttool,identical JSON-RPC exchange, wire bytes measured raw):
Test
McpJsonUtilitiesTests.DefaultOptions_KeepsNonAsciiUnescapedasserts a Cyrillic/CJK stringround-trips unescaped through
DefaultOptionson net8.0/net9.0/net10.0/net472.DockerEverythingServerTestsintegrationtests fail identically on a clean checkout: the pinned third-party image
tzolov/mcp-everything-server:v1(up to date as of this run) no longer exposes thetrigger-sampling-requesttool the test calls. Pre-existing environment drift, unrelated tothis change.
Addresses #626.